Odkryj obiekty wartości w modułach JavaScript dla solidnego i testowalnego kodu. Naucz się implementować niezmienne struktury danych i zwiększać integralność danych.
Obiekt wartości w modułach JavaScript: Niezmienne modelowanie danych
W nowoczesnym programowaniu w JavaScript zapewnienie integralności i łatwości utrzymania danych jest kluczowe. Jedną z potężnych technik, aby to osiągnąć, jest wykorzystanie obiektów wartości w modułowych aplikacjach JavaScript. Obiekty wartości, szczególnie w połączeniu z niezmiennością, oferują solidne podejście do modelowania danych, które prowadzi do czystszego, bardziej przewidywalnego i łatwiejszego do testowania kodu.
Czym jest obiekt wartości?
Obiekt wartości (Value Object) to mały, prosty obiekt, który reprezentuje pewną koncepcyjną wartość. W przeciwieństwie do encji, które są definiowane przez swoją tożsamość, obiekty wartości są definiowane przez swoje atrybuty. Dwa obiekty wartości uważa się za równe, jeśli ich atrybuty są równe, niezależnie od ich tożsamości obiektowej. Typowe przykłady obiektów wartości to:
- Waluta: Reprezentuje wartość pieniężną (np. 10 USD, 5 EUR).
- Zakres dat: Reprezentuje datę początkową i końcową.
- Adres e-mail: Reprezentuje prawidłowy adres e-mail.
- Kod pocztowy: Reprezentuje prawidłowy kod pocztowy dla danego regionu (np. 90210 w USA, SW1A 0AA w Wielkiej Brytanii, 10115 w Niemczech, 〒100-0001 w Japonii).
- Numer telefonu: Reprezentuje prawidłowy numer telefonu.
- Współrzędne: Reprezentują lokalizację geograficzną (szerokość i długość geograficzną).
Kluczowe cechy obiektu wartości to:
- Niezmienność (Immutability): Po utworzeniu stan obiektu wartości nie może być zmieniony. Eliminuje to ryzyko niezamierzonych efektów ubocznych.
- Równość oparta na wartości: Dwa obiekty wartości są równe, jeśli ich wartości są równe, a nie jeśli są tym samym obiektem w pamięci.
- Enkapsulacja: Wewnętrzna reprezentacja wartości jest ukryta, a dostęp do niej zapewniają metody. Pozwala to na walidację i zapewnia integralność wartości.
Dlaczego warto używać obiektów wartości?
Stosowanie obiektów wartości w aplikacjach JavaScript oferuje kilka znaczących korzyści:
- Poprawiona integralność danych: Obiekty wartości mogą egzekwować ograniczenia i reguły walidacji w momencie tworzenia, zapewniając, że używane są tylko prawidłowe dane. Na przykład obiekt wartości `AdresEmail` może zweryfikować, czy podany ciąg znaków jest rzeczywiście prawidłowym formatem e-mail. Zmniejsza to ryzyko rozprzestrzeniania się błędów w systemie.
- Zredukowane efekty uboczne: Niezmienność eliminuje możliwość niezamierzonych modyfikacji stanu obiektu wartości, co prowadzi do bardziej przewidywalnego i niezawodnego kodu.
- Uproszczone testowanie: Ponieważ obiekty wartości są niezmienne, a ich równość opiera się na wartości, testy jednostkowe stają się znacznie łatwiejsze. Można po prostu tworzyć obiekty wartości o znanych wartościach i porównywać je z oczekiwanymi wynikami.
- Zwiększona czytelność kodu: Obiekty wartości sprawiają, że kod jest bardziej wyrazisty i łatwiejszy do zrozumienia, ponieważ jawnie reprezentują pojęcia domenowe. Zamiast przekazywać surowe ciągi znaków lub liczby, można używać obiektów wartości, takich jak `Waluta` czy `KodPocztowy`, co czyni intencje kodu jaśniejszymi.
- Ulepszona modułowość: Obiekty wartości hermetyzują specyficzną logikę związaną z daną wartością, promując rozdzielenie odpowiedzialności i czyniąc kod bardziej modułowym.
- Lepsza współpraca: Używanie standardowych obiektów wartości promuje wspólne zrozumienie w zespołach. Na przykład, każdy rozumie, co reprezentuje obiekt 'Waluta'.
Implementacja obiektów wartości w modułach JavaScript
Przyjrzyjmy się, jak zaimplementować obiekty wartości w JavaScript przy użyciu modułów ES, koncentrując się na niezmienności i prawidłowej enkapsulacji.
Przykład: Obiekt wartości AdresEmail
Rozważmy prosty obiekt wartości `AdresEmail`. Użyjemy wyrażenia regularnego do walidacji formatu adresu e-mail.
```javascript // email-address.js const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; class EmailAddress { constructor(value) { if (!EmailAddress.isValid(value)) { throw new Error('Invalid email address format.'); } // Właściwość prywatna (z użyciem domknięcia) let _value = value; this.getValue = () => _value; // Getter // Zapobiegaj modyfikacji spoza klasy Object.freeze(this); } getValue() { return this.value; } toString() { return this.getValue(); } static isValid(value) { return EMAIL_REGEX.test(value); } equals(other) { if (!(other instanceof EmailAddress)) { return false; } return this.getValue() === other.getValue(); } } export default EmailAddress; ```Wyjaśnienie:
- Eksport modułu: Klasa `EmailAddress` jest eksportowana jako moduł, co umożliwia jej ponowne użycie w różnych częściach aplikacji.
- Walidacja: Konstruktor waliduje wejściowy adres e-mail za pomocą wyrażenia regularnego (`EMAIL_REGEX`). Jeśli adres e-mail jest nieprawidłowy, zgłaszany jest błąd. Zapewnia to, że tworzone są tylko prawidłowe obiekty `EmailAddress`.
- Niezmienność: `Object.freeze(this)` zapobiega wszelkim modyfikacjom obiektu `EmailAddress` po jego utworzeniu. Próba modyfikacji zamrożonego obiektu spowoduje błąd. Używamy również domknięć, aby ukryć właściwość `_value`, uniemożliwiając bezpośredni dostęp do niej spoza klasy.
- Metoda `getValue()`: Metoda `getValue()` zapewnia kontrolowany dostęp do bazowej wartości adresu e-mail.
- Metoda `toString()`: Metoda `toString()` pozwala na łatwą konwersję obiektu wartości do ciągu znaków.
- Metoda statyczna `isValid()`: Metoda statyczna `isValid()` pozwala sprawdzić, czy ciąg znaków jest prawidłowym adresem e-mail, bez tworzenia instancji klasy.
- Metoda `equals()`: Metoda `equals()` porównuje dwa obiekty `EmailAddress` na podstawie ich wartości, zapewniając, że równość jest określana przez zawartość, a nie tożsamość obiektu.
Przykład użycia
```javascript // main.js import EmailAddress from './email-address.js'; try { const email1 = new EmailAddress('test@example.com'); const email2 = new EmailAddress('test@example.com'); const email3 = new EmailAddress('invalid-email'); // To spowoduje zgłoszenie błędu console.log(email1.getValue()); // Wynik: test@example.com console.log(email1.toString()); // Wynik: test@example.com console.log(email1.equals(email2)); // Wynik: true // Próba modyfikacji email1 zgłosi błąd (wymagany tryb ścisły) // email1.value = 'new-email@example.com'; // Błąd: Cannot assign to read only property 'value' of object '#Zaprezentowane korzyści
Ten przykład demonstruje podstawowe zasady obiektów wartości:
- Walidacja: Konstruktor `EmailAddress` wymusza walidację formatu adresu e-mail.
- Niezmienność: Wywołanie `Object.freeze()` zapobiega modyfikacji.
- Równość oparta na wartości: Metoda `equals()` porównuje adresy e-mail na podstawie ich wartości.
Zaawansowane zagadnienia
Typescript
Chociaż poprzedni przykład używa czystego JavaScriptu, TypeScript może znacznie ulepszyć rozwój i solidność obiektów wartości. TypeScript pozwala definiować typy dla obiektów wartości, zapewniając sprawdzanie typów w czasie kompilacji i lepszą konserwację kodu. Oto jak można zaimplementować obiekt wartości `EmailAddress` przy użyciu TypeScript:
```typescript // email-address.ts const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; class EmailAddress { private readonly value: string; constructor(value: string) { if (!EmailAddress.isValid(value)) { throw new Error('Invalid email address format.'); } this.value = value; Object.freeze(this); } getValue(): string { return this.value; } toString(): string { return this.value; } static isValid(value: string): boolean { return EMAIL_REGEX.test(value); } equals(other: EmailAddress): boolean { return this.value === other.getValue(); } } export default EmailAddress; ```Kluczowe ulepszenia dzięki TypeScript:
- Bezpieczeństwo typów: Właściwość `value` jest jawnie otypowana jako `string`, a konstruktor wymusza przekazywanie tylko ciągów znaków.
- Właściwości tylko do odczytu: Słowo kluczowe `readonly` zapewnia, że właściwość `value` może być przypisana tylko w konstruktorze, co dodatkowo wzmacnia niezmienność.
- Lepsze uzupełnianie kodu i wykrywanie błędów: TypeScript zapewnia lepsze uzupełnianie kodu i pomaga wychwytywać błędy związane z typami na etapie programowania.
Techniki programowania funkcyjnego
Można również implementować obiekty wartości, stosując zasady programowania funkcyjnego. Takie podejście często polega na używaniu funkcji do tworzenia i manipulowania niezmiennymi strukturami danych.
```javascript // currency.js import { isNil, isNumber, isString } from 'lodash-es'; function Currency(amount, code) { if (!isNumber(amount)) { throw new Error('Amount must be a number'); } if (!isString(code) || code.length !== 3) { throw new Error('Code must be a 3-letter string'); } const _amount = amount; const _code = code.toUpperCase(); return Object.freeze({ getAmount: () => _amount, getCode: () => _code, toString: () => `${_code} ${_amount}`, equals: (other) => { if (isNil(other) || typeof other.getAmount !== 'function' || typeof other.getCode !== 'function') { return false; } return other.getAmount() === _amount && other.getCode() === _code; } }); } export default Currency; // Przykład // const price = Currency(19.99, 'USD'); ```Wyjaśnienie:
- Funkcja fabrykująca (Factory Function): Funkcja `Currency` działa jak fabryka, tworząc i zwracając niezmienny obiekt.
- Domknięcia (Closures): Zmienne `_amount` i `_code` są zamknięte w zasięgu funkcji, co czyni je prywatnymi i niedostępnymi z zewnątrz.
- Niezmienność: `Object.freeze()` zapewnia, że zwrócony obiekt nie może być modyfikowany.
Serializacja i deserializacja
Podczas pracy z obiektami wartości, zwłaszcza w systemach rozproszonych lub podczas przechowywania danych, często trzeba je serializować (konwertować do formatu tekstowego, jak JSON) i deserializować (konwertować z formatu tekstowego z powrotem na obiekt wartości). Podczas serializacji do formatu JSON zazwyczaj otrzymuje się surowe wartości, które reprezentują obiekt wartości (reprezentację w postaci `string`, `number` itp.).
Podczas deserializacji należy upewnić się, że zawsze odtwarza się instancję obiektu wartości za pomocą jego konstruktora, aby wymusić walidację i niezmienność.
```javascript // Serializacja const email = new EmailAddress('test@example.com'); const emailJSON = JSON.stringify(email.getValue()); // Serializuj bazową wartość console.log(emailJSON); // Wynik: "test@example.com" // Deserializacja const deserializedEmail = new EmailAddress(JSON.parse(emailJSON)); // Odtwórz obiekt wartości console.log(deserializedEmail.getValue()); // Wynik: test@example.com ```Przykłady z życia wzięte
Obiekty wartości można stosować w różnych scenariuszach:
- E-commerce: Reprezentowanie cen produktów za pomocą obiektu wartości `Waluta`, zapewniając spójną obsługę walut. Walidacja numerów SKU produktów za pomocą obiektu wartości `SKU`.
- Aplikacje finansowe: Obsługa kwot pieniężnych i numerów kont za pomocą obiektów wartości `Pieniądze` i `NumerKonta`, wymuszając reguły walidacji i zapobiegając błędom.
- Aplikacje geograficzne: Reprezentowanie współrzędnych za pomocą obiektu wartości `Współrzędne`, zapewniając, że wartości szerokości i długości geograficznej mieszczą się w prawidłowych zakresach. Reprezentowanie krajów za pomocą obiektu wartości `KodKraju` (np. "US", "GB", "DE", "JP", "BR").
- Zarządzanie użytkownikami: Walidacja adresów e-mail, numerów telefonów i kodów pocztowych za pomocą dedykowanych obiektów wartości.
- Logistyka: Obsługa adresów wysyłkowych za pomocą obiektu wartości `Adres`, zapewniając, że wszystkie wymagane pola są obecne i prawidłowe.
Korzyści wykraczające poza kod
- Lepsza współpraca: Obiekty wartości definiują wspólne słownictwo w zespole i projekcie. Kiedy wszyscy rozumieją, co reprezentuje `KodPocztowy` lub `NumerTelefonu`, współpraca jest znacznie ułatwiona.
- Łatwiejsze wdrażanie: Nowi członkowie zespołu mogą szybko zrozumieć model domeny, poznając cel i ograniczenia każdego obiektu wartości.
- Zmniejszone obciążenie poznawcze: Dzięki hermetyzacji złożonej logiki i walidacji w obiektach wartości, programiści mogą skupić się na logice biznesowej wyższego poziomu.
Dobre praktyki dla obiektów wartości
- Utrzymuj je małymi i skoncentrowanymi: Obiekt wartości powinien reprezentować jedno, dobrze zdefiniowane pojęcie.
- Wymuszaj niezmienność: Zapobiegaj modyfikacjom stanu obiektu wartości po jego utworzeniu.
- Implementuj równość opartą na wartości: Upewnij się, że dwa obiekty wartości są uważane za równe, jeśli ich wartości są równe.
- Zapewnij metodę `toString()`: Ułatwia to reprezentowanie obiektów wartości jako ciągów znaków na potrzeby logowania i debugowania.
- Pisz kompleksowe testy jednostkowe: Dokładnie testuj walidację, równość i niezmienność swoich obiektów wartości.
- Używaj znaczących nazw: Wybieraj nazwy, które jasno odzwierciedlają pojęcie reprezentowane przez obiekt wartości (np. `AdresEmail`, `Waluta`, `KodPocztowy`).
Podsumowanie
Obiekty wartości oferują potężny sposób modelowania danych w aplikacjach JavaScript. Przyjmując niezmienność, walidację i równość opartą na wartości, można tworzyć bardziej solidny, łatwiejszy w utrzymaniu i testowalny kod. Niezależnie od tego, czy budujesz małą aplikację internetową, czy system korporacyjny na dużą skalę, włączenie obiektów wartości do architektury może znacznie poprawić jakość i niezawodność oprogramowania. Używając modułów do organizowania i eksportowania tych obiektów, tworzysz wysoce reużywalne komponenty, które przyczyniają się do bardziej modułowej i dobrze zorganizowanej bazy kodu. Zastosowanie obiektów wartości to znaczący krok w kierunku budowania czystszych, bardziej niezawodnych i łatwiejszych do zrozumienia aplikacji JavaScript dla globalnej publiczności.